package com.dhnsoft.shortLink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dhnsoft.shortLink.project.common.convention.exception.ClientException;
import com.dhnsoft.shortLink.project.common.convention.exception.ServiceException;
import com.dhnsoft.shortLink.project.common.enums.ValidDateTypeEnum;
import com.dhnsoft.shortLink.project.dao.entity.*;
import com.dhnsoft.shortLink.project.dao.mapper.*;
import com.dhnsoft.shortLink.project.dto.req.ShortLinkCreateReqDTO;
import com.dhnsoft.shortLink.project.dto.req.ShortLinkPageReqDTO;
import com.dhnsoft.shortLink.project.dto.req.ShortLinkUpdateReqDTO;
import com.dhnsoft.shortLink.project.dto.resp.ShortLinkCountQueryRespDTO;
import com.dhnsoft.shortLink.project.dto.resp.ShortLinkCreateRespDTO;
import com.dhnsoft.shortLink.project.dto.resp.ShortLinkPageRespDTO;
import com.dhnsoft.shortLink.project.service.ShortLinkService;
import com.dhnsoft.shortLink.project.toolkit.HashUtil;
import com.dhnsoft.shortLink.project.toolkit.LinkUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.dhnsoft.shortLink.project.common.constant.RedisKeyConstant.*;
import static com.dhnsoft.shortLink.project.common.constant.ShortLinkConstant.AMAP_REMOTE_URL;

/**
 * ClassName: ShortLinkServiceImpl
 * Package: com.dhnsoft.shortLink.project.service.impl
 * Description:
 *
 * @Author dhn
 * @Create 2024/3/21 17:38
 * @Version 1.0
 */
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;

    private final ShortLinkGotoMapper shortLinkGotoMapper;

    private final StringRedisTemplate stringRedisTemplate;

    private final RedissonClient redissonClient;

    private final LinkAccessStatsMapper linkAccessStatsMapper;

    private final LinkLocaleStatsMapper linkLocaleStatsMapper;

    private final LinkOsStatsMapper linkOsStatsMapper;

    private final LinkBrowserStatsMapper linkBrowserStatsMapper;

    private final LinkAccessLogsMapper linkAccessLogsMapper;

    private final LinkDeviceStatsMapper linkDeviceStatsMapper;

    private final LinkNetworkStatsMapper linkNetworkStatsMapper;

    @Value("${short-link.stats.locale.amap-key}")
    private String statsLocaleAmapKey;

    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        String shortLinkSuffix = generateSuffix(requestParam);
        ShortLinkDO shortLinkDO = BeanUtil.toBean(requestParam, ShortLinkDO.class);
        String fullShortUrl = shortLinkDO.getDomain()+"/"+shortLinkSuffix;
        shortLinkDO.setFullShortUrl(fullShortUrl);
        shortLinkDO.setEnableStatus(0);
        shortLinkDO.setShortUri(shortLinkSuffix);
        shortLinkDO.setFavicon(getFavicon(requestParam.getOriginUrl()));
        try {
            baseMapper.insert(shortLinkDO);
            ShortLinkGotoDO shortLinkDo = ShortLinkGotoDO.builder()
                    .gid(requestParam.getGid())
                    .fullShortUrl(fullShortUrl)
                    .build();
            shortLinkGotoMapper.insert(shortLinkDo);
        }
        catch (DuplicateKeyException ex){
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl);
            ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
            if(hasShortLinkDO!=null){
                log.warn("短链接: {} 重复入库");
                throw new ServiceException("短链接已存在");
            }
        }
        stringRedisTemplate.opsForValue().set(
                String.format(GOTO_SHORT_LINK_KEY,fullShortUrl),requestParam.getOriginUrl(),
                LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()),TimeUnit.MILLISECONDS
        );
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDTO.builder()
                .fullShortURL("http://"+shortLinkDO.getFullShortUrl())
                .gid(requestParam.getGid())
                .domain(requestParam.getDomain())
                .originUrl(requestParam.getOriginUrl())
                .build();
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getGid())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        IPage<ShortLinkDO> resultPage =  baseMapper.selectPage(requestParam, queryWrapper);
        return resultPage.convert(each->{
            ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            result.setDomain("http://"+result.getDomain());
            return result;
        });
    }

    @Override
    public List<ShortLinkCountQueryRespDTO> listShortLinkCount(List<String> requestParam) {
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid,count(*) as shortLinkCount")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .groupBy("gid");
        List<Map<String, Object>> maps = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(maps,ShortLinkCountQueryRespDTO.class);
    }

    @Override
    @Transactional
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        //查询出需要修改的数据及其字段
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getGid())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        if(hasShortLinkDO==null){
            throw new ClientException("短链接记录不存在");
        }
        //实例化出需要修改的新ShortLinkDO
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .gid(requestParam.getGid())
                .shortUri(hasShortLinkDO.getShortUri())
                .createdType(hasShortLinkDO.getCreatedType())
                .clickNum(hasShortLinkDO.getClickNum())
                .describe(requestParam.getDescribe())
                .favicon(hasShortLinkDO.getFavicon())
                .originUrl(requestParam.getOriginUrl())
                .validDate(requestParam.getValidDate())
                .validDateType(requestParam.getValidDateType())
                .domain(hasShortLinkDO.getDomain())
                .build();
        //判断老短链接gid对比新短链接gid是否相同，若相同则删除掉旧的gid（因为通过shardingSphere分表是通过gid来分的，若修改gid，会丢失这条数据）
        if(Objects.equals(requestParam.getGid(),hasShortLinkDO.getGid())){
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(Objects.equals(requestParam.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()), ShortLinkDO::getValidDate, null);
            baseMapper.update(shortLinkDO,updateWrapper);
        }
        else{
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0);
            baseMapper.delete(updateWrapper);
            baseMapper.insert(shortLinkDO);
        }

    }

    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, HttpServletRequest request, HttpServletResponse response) {
        String serverName = request.getServerName();
        String fullShortUrl = serverName + "/" +shortUri;
        //先在redis缓存中查询是否有该完整短链接缓存
        String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        //如果缓存不为空则直接跳转
        if(StrUtil.isNotBlank(originalLink)){
            shortLinkStats(fullShortUrl,null,request,response);
            response.sendRedirect(originalLink);
            return;
        }
        //查询布隆过滤器是否存在完整短链接，若不存在，则数据库一定没有
        boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        if(!contains){
            response.sendRedirect("/page/notfound");
            return;
        }
        //布隆过滤器可能产生误判，再查询缓存是否存在空值，若缓存中存在空值，则数据库一定没有，返回为空
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isNotBlank(gotoIsNullShortLink)){
            response.sendRedirect("/page/notfound");
            return;
        }
        //防止缓存击穿问题：防止热点短链接有效期刚好在缓存中过期后，被疯狂请求数据库，造成缓存击穿问题
        //使用redis分布式锁
        RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        lock.lock();
        try {
            //再次查询redis缓存，防止高并发下查询缓存出现问题
            //双检加锁策略
            originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            //若不为空直接返回
            if(StrUtil.isNotBlank(originalLink)){
                shortLinkStats(fullShortUrl,null,request,response);
                response.sendRedirect(originalLink);
                return;
            }
            //若为空则查询数据库
            LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
            if(shortLinkGotoDO == null){
                //若数据库也为空，则需要在缓存中设置一个短链接空值缓存,过期时间为30分钟
                stringRedisTemplate.opsForValue().set(GOTO_IS_NULL_SHORT_LINK_KEY,"-",30, TimeUnit.MINUTES);
                response.sendRedirect("/page/notfound");
                return;
            }
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
            if(shortLinkDO==null || (shortLinkDO.getValidDate()!=null&&shortLinkDO.getValidDate().before(new Date()))){
                //当查询数据库中的有效时间不为null（证明是无限有效期）或者有效期超时，则设置空值跳转缓存
                stringRedisTemplate.opsForValue().set(GOTO_IS_NULL_SHORT_LINK_KEY,"-",30, TimeUnit.MINUTES);
                response.sendRedirect("/page/notfound");
                return;
            }
            //查询不为空则插入到缓存中
            stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),shortLinkDO.getOriginUrl()
                    ,LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),TimeUnit.MILLISECONDS);
            shortLinkStats(fullShortUrl,shortLinkDO.getGid(),request,response);
            response.sendRedirect(shortLinkDO.getOriginUrl());
        }
        finally {
            lock.unlock();
        }

    }

    public void shortLinkStats(String fullShortUrl,String gid,HttpServletRequest request, HttpServletResponse response){
        //首先获取当前链接的Cookie
        Cookie[] cookies = request.getCookies();
        //设置一个原子布尔类型
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        try {
            /**
             * 计算UV操作，同一用户访问不加
             * */
            AtomicReference<String> uv = new AtomicReference<>();
            //首先需要创建一个cookie记录，作为新用户访问生产cookie记录
            Runnable addResponseCookieTask=()->{
              String actualUv = UUID.fastUUID().toString();
              uv.set(actualUv);
              //将生成的UUID作为cookie的value插入到cookie中
              Cookie uvCookie = new Cookie("uv",uv.get());
              //设置cookie的过期时间,设置成一个月
                uvCookie.setMaxAge(60*60*24*30);
                //cookie的路径为短链接uri
                uvCookie.setPath(StrUtil.sub(fullShortUrl,fullShortUrl.indexOf("/"),fullShortUrl.length()));
                response.addCookie(uvCookie);
                //当前是新用户访问，则需要将uvFirstFlag 设置成true
                uvFirstFlag.set(true);
                //将当前的cookie值设置到redis中
                stringRedisTemplate.opsForSet().add("short-link:stats:uv"+fullShortUrl,uv.get());
            };
            //判断当前是否能获取到cookie
            //若当前cookies不为空
            if(ArrayUtil.isNotEmpty(cookies)){
                Arrays.stream(cookies)
                        //过滤保留cookie数组中名称相同的cookie
                        .filter(each->Objects.equals(each.getName(),"uv"))
                        .findFirst()
                        .ifPresentOrElse(each->{
                            uv.set(each.getValue());
                            //将当前的值加入到redis的set集合内，若增加成功，说明cookie不存在，返回uvFirstFlag为true
                            Long added = stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, each.getValue());
                            uvFirstFlag.set(added!=null&&added>0L);
                        },addResponseCookieTask);
            }
            else{
                //若cookie不为空直接插入
                addResponseCookieTask.run();
            }
            String remoteAddr = LinkUtil.getIp(request);
            Long uipAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uip:" + fullShortUrl, remoteAddr);
            boolean uipFirstFlag = uipAdded!=null&&uipAdded>0L;

            /**
             * 计算PV操作，访问一次则加1
             * */
            if(StrUtil.isBlank(gid)){
                LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);
                gid = shortLinkGotoDO.getGid();
            }
            Date date = new Date();
            int hour = DateUtil.hour(date, true);
            int weekDay = DateUtil.dayOfWeek(date);
            // 转换为常规星期数（星期天为1，星期一为2...）
            weekDay = convertToRegularWeekday(weekDay);
            LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                    .pv(1)
                    .uv(uvFirstFlag.get()?1:0)
                    .uip(uipFirstFlag?1:0)
                    .date(date)
                    .weekday(weekDay)
                    .hour(hour)
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .build();
            linkAccessStatsMapper.shortLinkStats(linkAccessStatsDO);

            /**
             * 通过ip获取地区详情
             * */
            Map<String,Object> localeParamMap = new HashMap<>();
            localeParamMap.put("key",statsLocaleAmapKey);
            localeParamMap.put("ip",remoteAddr);
            //调用高德的IP定位API服务地址，传入key和ip
            String localeResultObj = HttpUtil.get(AMAP_REMOTE_URL, localeParamMap);
            //将高德返回的JSON转换为JSONObject格式
            JSONObject jsonObject = JSON.parseObject(localeResultObj);
            //返回状态码
            String infocode = jsonObject.getString("infocode");
            String actualProvince;
            String actualCity;
            if(StrUtil.isNotBlank(infocode)&&Objects.equals(infocode,"10000")){
                String province = jsonObject.getString("province");
                String city = jsonObject.getString("city");
                String adcode = jsonObject.getString("adcode");
                boolean unknownFlag = StrUtil.equals(province,"[]");
                LinkLocaleStatsDO linkLocaleStats = LinkLocaleStatsDO.builder()
                        .province(actualProvince = unknownFlag ? "未知" : province)
                        .cnt(1)
                        .country("中国")
                        .city(actualCity = unknownFlag ? "未知" : city)
                        .adcode(unknownFlag ? "未知" : adcode)
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .date(date)
                        .build();
                linkLocaleStatsMapper.linkLocaleStats(linkLocaleStats);

                /**
                 * 统计短链接操作系统访问
                 * */
                String os = LinkUtil.getOs(request);
                LinkOsStatsDO linkOsStatsDos = LinkOsStatsDO.builder()
                        .gid(gid)
                        .cnt(1)
                        .fullShortUrl(fullShortUrl)
                        .date(date)
                        .os(os)
                        .build();
                linkOsStatsMapper.linkOsStats(linkOsStatsDos);

                /**
                 * 统计短链接浏览器访问
                 * */
                String browser = LinkUtil.getBrowser(request);
                LinkBrowserStatsDO linkBrowserStats = LinkBrowserStatsDO.builder()
                        .gid(gid)
                        .cnt(1)
                        .fullShortUrl(fullShortUrl)
                        .date(date)
                        .browser(browser)
                        .build();
                linkBrowserStatsMapper.linkBrowserStats(linkBrowserStats);

                /**
                 * 统计短链接日志监控
                 * */
                LinkAccessLogsDO linkAccessLogs = LinkAccessLogsDO.builder()
                        .ip(remoteAddr)
                        .user(uv.get())
                        .browser(browser)
                        .device(LinkUtil.getDevice(request))
                        .network(LinkUtil.getNetwork(request))
                        .locale(StrUtil.join("-","中国",actualProvince,actualCity))
                        .os(os)
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .build();
                linkAccessLogsMapper.insert(linkAccessLogs);

                /**
                 * 短链接访问设备信息
                 * */
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .cnt(1)
                        .date(date)
                        .device(LinkUtil.getDevice(request))
                        .build();
                linkDeviceStatsMapper.linkDeviceStatsDO(linkDeviceStatsDO);

                /**
                 * 短链接访问网络信息
                 * */
                LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .cnt(1)
                        .date(date)
                        .network(LinkUtil.getNetwork(request))
                        .build();
                linkNetworkStatsMapper.linkNetworkStatsDO(linkNetworkStatsDO);

            }


        }
        catch (Throwable ex){
            log.error("短链接统计异常",ex);
        }
    }
    private int convertToRegularWeekday(int isoWeekday) {
        switch (isoWeekday){
            case 1:return 7;
            case 2:return 1;
            case 3:return 2;
            case 4:return 3;
            case 5:return 4;
            case 6:return 5;
            case 7:return 6;
            default:return -1;
        }
    }

    private String generateSuffix(ShortLinkCreateReqDTO requestParam){
        int customGenerateCount = 0;
        String shortUri;
        while (true){
            if(customGenerateCount>10){
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            String originalUrl = requestParam.getOriginUrl();
            originalUrl+=System.currentTimeMillis();
            shortUri = HashUtil.hashToBase62(originalUrl);
            if(!shortUriCreateCachePenetrationBloomFilter.contains(requestParam.getDomain()+"/"+shortUri)){
                break;
            }
            customGenerateCount++;
        }
        return shortUri;
    }


    /**
     * 获取原始链接图标
     * */
    @SneakyThrows
    private String getFavicon(String url){
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if(responseCode==HttpURLConnection.HTTP_OK){
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if(faviconLink!=null){
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }

}
